S01-08 JavaSE-面向对象-基础
[TOC]
概述
入门案例:养猫猫问题
养猫猫问题:张老太养了两只猫猫:一只名字叫小白,今年 3 岁,白色;另一只叫小花,今年 100 岁,花色。编写程序,当用户输入小猫的名字时,显示该猫的名字、年龄、颜色;若输入错误,显示“张老太没有这只猫猫”。
现有技术解决
方式 1:单独定义变量
// 第1只猫信息
String cat1Name = "小白";
int cat1Age = 3;
String cat1Color = "白色";
// 第2只猫信息
String cat2Name = "小花";
int cat2Age = 100;
String cat2Color = "花色";缺点:不利于数据管理,变量过多易混乱。
方式 2:使用数组
// 第1只猫信息
String[] cat1 = {"小白", "3", "白色"};
// 第2只猫信息
String[] cat2 = {"小花", "100", "花色"};缺点:
- 数据类型不明确(年龄是字符串类型)
- 只能通过下标获取信息,对应关系不清晰
- 不能体现猫的行为(如叫、跑)
现有技术缺点
现有技术不利于数据管理、效率低,引出面向对象编程(OOP)。
面向对象解决
实现思路:定义Cat类(自定义数据类型),包含属性(名字、年龄、颜色)和行为,通过创建对象管理每只猫的信息。
public class Object01 {
// 编写一个main方法
public static void main(String[] args) {
// 2. 实例化 Cat 类
// 实例化第一只猫(创建对象),并赋值给 cat1 变量
Cat cat1 = new Cat();
cat1.name = "小白";
cat1.age = 3;
cat1.color = "白色";
cat1.weight = 10;
// 实例化第二只猫,并赋值给 cat1 变量
Cat cat2 = new Cat();
cat2.name = "小花";
cat2.age = 100;
cat2.color = "花色";
cat2.weight = 20;
// 3. 访问对象属性
System.out.println("第1只猫信息:" + cat1.name + " " + cat1.age + " " + cat1.color + " " + cat1.weight);
System.out.println("第2只猫信息:" + cat2.name + " " + cat2.age + " " + cat2.color + " " + cat2.weight);
}
}
// 1. 定义猫类 Cat(自定义数据类型)
class Cat {
// 属性(成员变量)
String name; // 名字
int age; // 年龄
String color; // 颜色
double weight; // 体重
// 行为(后续补充)
}面向对象编程
面向对象编程(OOP,Object-Oriented Programming):是一种以对象为核心的编程范式,它将现实世界中的事物抽象为程序中的对象,通过封装对象的属性和行为、建立对象间的继承与多态关系,来组织和构建程序。
Java 是纯粹的面向对象语言(除基本数据类型外,所有事物都可抽象为对象,且基本类型也有对应的包装类),OOP 是 Java 的核心思想,其核心在于类与对象,并通过封装、继承、多态三大特性实现代码的高复用、易维护和可扩展。
类与对象
类(Class):对象的 “模板” 或 “蓝图”,是对一类具有相同 属性(状态) 和方法(行为) 的事物的抽象描述。
- 属性:也叫成员变量 / 字段(Field),表示对象的状态(比如人的姓名、年龄)。
- 方法:也叫成员方法(Method),表示对象的行为(比如人的吃饭、跑步)。
对象(Object):类的 “实例”,是类的具体实现,是内存中实际存在的实体。通过new关键字可以创建类的对象,每个对象都拥有类定义的属性和方法的独立副本。
类与对象的关系图
猫类(Cat)→ 提取共性:属性(name, age, color)、行为(run, cry...)
对象1(小白):name="小白",age=3,color="白色"
对象2(小花):name="小花",age=100,color="花色"对象内存图
对象在内容中的存在形式如图:

基础
属性
属性(成员变量,字段,Field):表示对象的状态(比如人的姓名、年龄)。
定义语法:
访问修饰符:public、protected、默认、private,用于控制属性的访问范围。
访问修饰符 属性类型 属性名;
// 示例
class Car {
protected String name;
}注意事项:
属性的类型:属性可以是基本数据类型(int、double)或引用类型(对象、数组)。
javaclass Car { // 基本类型 double price; // 引用类型 String name; String color; String[] masters; }属性的默认值:未赋值的属性有默认值,规则同数组:
- 基本类型:
int 0、short 0、byte 0、long 0float 0.0、double 0.0char \u0000boolean false
- 引用类型(如 String):
null
java// 属性的默认值 public class PropertiesDetail { public static void main(String[] args) { Person p1 = new Person(); System.out.println( "age=" + p1.age + " name=" + p1.name + " sal=" + p1.sal + " isPass=" + p1.isPass ); // age=0,name=null,sal=0.0,isPass=false } } class Person { int age; // 默认值:0 String name; // 默认值:null double sal; // 默认值:0.0 boolean isPass; // 默认值:false }- 基本类型:
对象创建
对象创建有以下 2 种方式:
方式 1:先声明再创建
javaCat cat; // 声明对象(栈中存储引用) cat = new Cat(); // 创建对象(堆中分配空间)方式 2:直接创建
javaCat cat = new Cat();
访问属性
语法:
对象名.属性名;示例:属性赋值/取值
cat.name = "小白"; // 赋值
System.out.println(cat.age); // 取值内存流程图:对象创建
示例代码:
Person p1 = new Person();
p1.age = 10;
p1.name = "小明";
Person p2 = p1; // p2指向p1引用的对象
System.out.println(p2.age); // 输出10(p1和p2操作同一个对象)创建对象的流程:
以Person p = new Person();为例:
- 加载
Person类信息(属性和方法,仅加载一次)。 - 在堆中分配空间,进行默认初始化(属性赋默认值)。
- 将堆中对象地址赋给栈中的引用变量
p。 - 进行指定初始化(如
p.name = "jack")。
内存流程图:
Person p1 = new Person();
p1.age = 10; p1.name = "小明";
Person p2 = p1;
System.out.println(p2.age);
练习:对象内存流程图
练习:画出以下代码的内存流程图
Person a = new Person();
a.age = 10;
a.name = "小明";
Person b;
b = a;
System.out.println(b.name); // "小明"
b.age = 200;
b = null;
System.out.println(a.age); // 200
System.out.println(b.age); // 抛出异常:java.lang.NullPointerException
成员方法
方法(Method,成员方法):表示对象的行为(比如人的吃饭、跑步)。
快速入门
给 Person 类添加方法:
speak():输出“我是一个好人”cal01():计算 1+2+...+1000 的结果cal02(int n):计算 1+2+...+n 的结果getSum(int num1, int num2):计算两个数的和
public class Method01 {
public static void main(String[] args) {
// 创建Person对象
Person p1 = new Person();
// 调用方法
p1.speak(); // 输出"我是一个好人"
p1.cal01(); // 输出1+...+1000的结果
p1.cal02(5); // 输出1+...+5的结果
p1.cal02(10); // 输出1+...+10的结果
int sum = p1.getSum(10, 20); // 接收方法返回值
System.out.println("两数之和:" + sum);
}
}
class Person {
String name;
int age;
// 1. speak方法:无参数、无返回值
public void speak() {
System.out.println("我是一个好人");
}
// 2. cal01方法:无参数、无返回值
public void cal01() {
int res = 0;
for (int i = 1; i <= 1000; i++) {
res += i;
}
System.out.println("1+...+1000的结果:" + res);
}
// 3. cal02方法:有参数、无返回值
public void cal02( int n ) { // 形参
int res = 0;
for (int i = 1; i <= n; i++) {
res += i;
}
System.out.println("1+...+" + n + "的结果:" + res);
}
// 4. getSum方法:有参数、有返回值
public int getSum(int num1, int num2) { // 形参列表
return num1 + num2; // 返回值
}
}语法结构
语法格式
一个完整的 Java 成员方法由多个组成部分构成,语法格式如下:
[修饰符列表] [返回值类型] 方法名([参数列表]) [throws 异常类型列表] {
// 方法体:具体的功能逻辑代码
[return 返回值]
}下面对每个组成部分进行详细解析:
修饰符列表
修饰符分为访问修饰符和非访问修饰符,可组合使用(需遵循语法规则,比如abstract和static不能同时修饰一个方法)。
访问修饰符
| 修饰符 | 可见范围 | 说明 |
|---|---|---|
public | 所有类(跨包、跨类) | 最宽松的访问权限 |
protected | 本类、同包子类、不同包子类 | 继承相关的访问权限 |
default | 本类、同包类(无修饰符) | 默认访问权限 |
private | 仅本类 | 最严格的访问权限 |
非访问修饰符
| 修饰符 | 作用 |
|---|---|
static | 定义类方法(静态方法),属于类而非对象 |
final | 方法不能被子类重写(Override) |
abstract | 定义抽象方法,无方法体,仅声明方法签名(只能在抽象类/接口中) |
native | 本地方法,方法体由非 Java 代码(如 C/C++)实现,仅声明签名 |
synchronized | 同步方法,保证多线程环境下的原子性(避免线程安全问题) |
strictfp | 严格浮点计算,保证不同平台下浮点运算结果一致 |
返回值
核心特性:
方法执行完成后返回给调用者的结果类型,分为两种情况:
有返回值:
可以是 Java 的基本数据类型(
int/double等)、引用数据类型(String/Object/自定义类等)。此时方法体中必须通过
return语句返回对应类型(或类型兼容)的值,且return后不能有多余代码。


无返回值:用
void表示,方法体中可以省略return语句(或写return;表示结束方法)。java// 无返回值:仅打印信息 public void printInfo(String msg) { System.out.println(msg); // 可省略return,也可写return; }返回值数量:一个方法最多有一个返回值,如需返回多个结果,可使用数组


方法名
遵循 Java 的命名规范:
- 采用小驼峰命名法(首字母小写,后续单词首字母大写,如
eatFood、calculateSum)。 - 见名知意,准确描述方法的功能(如
getAge表示获取年龄,setName表示设置姓名)。 - 不能与关键字重名,且不能包含空格、特殊符号(除
_和$)。
参数列表
基本格式
基本格式:多个参数用逗号分隔,每个参数的格式为:
// 基本格式
参数类型 参数名
// 示例
int a, String b核心特性:
参数个数:一个方法可以有 0 个参数,也可以有多个参数,参数中间用
,逗号间隔。参数类型:参数可以为任意类型(包括基本类型和引用类型)。
形参/实参:
- 实参:调用方法时传递给形参的具体值。
- 形参:定义方法时的参数称为形参,
- 要求**实参的类型、个数、顺序必须与形参兼容**(自动类型转换除外,如
int可赋值给double)。
javaclass AA { // 1. 方法定义(形参) public int[] getSumAndSub(int n1, int n2) { // 形参 int[] resArr = new int[2]; resArr[0] = n1 + n2; resArr[1] = n1 - n2; return resArr; } } public class MethodDetail { public static void main(String[] args) { // 2. 方法调用(实参) AA a = new AA();// ✅ 兼容:int -> int a.getSumAndSub(10, 4); // 实参 // ✅ 兼容:byte -> int byte b1 = 1, b2 = 2; a.getSumAndSub(b1, b2); // 实参 // ❌ 不兼容:double -> int a.getSumAndSub(1.1, 1.2); // 实参 } }
可变参数(不定长参数)
JDK 5 引入的特性,允许方法接收任意个数的同类型参数,语法为:参数类型... 参数名(本质是数组)。
- 可变参数必须放在参数列表的最后一位。
- 一个方法只能有一个可变参数。
示例:
// 可变参数:计算多个int的和
public int sum(int... nums) {
int total = 0;
for (int num : nums) { // 可变参数可当作数组遍历
total += num;
}
return total;
}
// 调用:可传入任意个数的int
sum(1, 2); // 结果3
sum(1, 2, 3, 4); // 结果10异常声明(throws)
声明方法可能抛出的受检异常(Checked Exception),告诉调用者需要处理这些异常(要么try-catch捕获,要么继续throws)。
示例:
// 声明方法可能抛出IOException
public void readFile(String path) throws IOException {
// 读取文件的逻辑,可能抛出IOException
}方法体
方法的核心逻辑代码块,包含变量定义、语句执行、流程控制(分支、循环)、调用其他方法等。
核心特性:
访问成员/局部变量:
方法体中可以访问类的成员变量(实例变量/静态变量)和局部变量(方法内定义的变量)。
局部变量必须先声明并初始化才能使用,而成员变量有默认值(如
int默认 0,String默认null)。
方法不能嵌套定义(不同于 JS)
javaclass Demo { // ❌ 方法不能嵌套定义 public void fn1() { public void fn2() {} } }
内存流程图:方法调用
示例代码:
public static void main(String[] args) {
Person p1 = new Person();
int sum = p1.getSum(10, 20);
System.out.println("两数之和:" + sum);
}
class Person {
public int getSum(int num1, int num2) {
return num1 + num2;
}
}方法调用流程:
- 执行方法时,JVM 会开辟独立的栈空间(方法栈)。
- 方法执行完毕或遇到
return时,栈空间释放,返回调用处。 - 返回后继续执行方法后面的代码。
- 当 main 方法(栈)执行完毕后,整个程序退出。
内存流程图:

成员方法作用
成员方法作用:
- 提高代码复用性:避免重复编写相同逻辑。
- 封装实现细节:用户无需关心内部逻辑,直接调用。
示例:封装二维数组遍历方法
定义MyTools类的printArr方法,实现二维数组遍历:
public class Method02 {
public static void main(String[] args) {
int[][] map = {{0,0,1},{1,1,1},{1,1,3}};
// 2. 多次调用方法,复用代码
MyTools tool = new MyTools();
tool.printArr(map);
tool.printArr(map);
}
}
class MyTools {
// 1. 封装方法:遍历二维数组
public void printArr(int[][] map) {
for (int i = 0; i < map.length; i++) {
for (int j = 0; j < map[i].length; j++) {
System.out.print(map[i][j] + "\t");
}
System.out.println();
}
}
}方法调用
方法调用注意事项:
本类中方法调用:直接调用(如
sayOk()调用print())。javaclass A { public void print(int n) { System.out.println("print方法被调用,n=" + n); } public void sayOk() { print(10); // 本类中调用:直接调用 } }跨类方法调用:通过对象名调用(如
B类对象调用A类方法)。javaclass A { public void m1() { // 跨类调用:通过B类对象名调用 B b = new B(); b.hi(); } } class B { public void hi() { System.out.println("B类的hi方法被执行"); } }注意:访问修饰符【后续再说】
跨类的方法调用和方法的访问呢修饰符也有关系。
练习
练习 1:判断奇偶方法
编写
AA类的isOdd(int num)方法:判断一个数是奇数还是偶数,返回boolean。java// 定义方法:判断奇偶 public boolean isOdd(int num) { return num % 2 != 0; } // 调用方法 System.out.println(a.isOdd(5)); // true System.out.println(a.isOdd(4)); // false练习 2:打印图形方法
编写
AA类的print(int row, int col, char c)方法:根据行、列、字符,打印对应格式的图形。java// 定义方法:打印图形 public void print(int row, int col, char c) { for (int i = 0; i < row; i++) { for (int j = 0; j < col; j++) { System.out.print(c); } System.out.println(); // 换行 } } // 调用方法 a.print(4, 4, '#');
方法传参机制@
Java 的方法传参机制是面试高频考点,也是理解方法调用底层逻辑的核心。核心结论:Java 中只有一种传参方式 —— 值传递(Pass by Value),不存在 “引用传递(Pass by Reference)”。很多开发者误以为引用类型是 “引用传递”,本质是对 “值” 的定义理解偏差:引用类型传递的是 “对象引用地址的副本”,而非引用本身,这仍属于值传递范畴。
值传递的本质
值传递的核心:
调用方法时,JVM 会创建实参的副本,并将这个副本传递给方法的形参;方法内部对形参的所有修改,仅作用于副本,不会直接影响原始实参。
对比容易混淆的 “引用传递”(Java 不支持):
引用传递是将实参的内存地址直接传递给形参,形参和实参指向同一个内存位置,方法内修改形参会直接改变实参。
基本数据类型
底层原理:
基本数据类型(int/double/boolean等)的实参存储在栈内存中,传参时会复制实参的数值本身给形参:
形参是栈中新创建的局部变量,与实参完全独立;
方法内修改形参的数值,仅改变栈中副本,原实参不受影响。
代码示例 + 内存解析:
public class BasicTypeParam {
// 方法:修改形参a的值
public static void changeInt(int a) {
a = 10; // 仅修改栈中形参副本
System.out.println("方法内a的值:" + a); // 输出:10
}
public static void main(String[] args) {
int num = 5; // 实参num:栈中存储值5
changeInt(num); // 传递num的副本(5)给形参a
System.out.println("方法外num的值:" + num); // 输出:5(原实参未变)
}
}内存执行流程(文字图解):
main方法执行时,栈中创建变量num,赋值为5(栈地址:比如0x001,值:5);- 调用
changeInt(num)时,JVM 在栈中为形参a分配新空间(栈地址:0x002),将num的5复制给a; - 方法内执行
a=10,仅修改0x002地址的值为10,0x001地址的num仍为5; - 方法执行完毕,形参
a出栈销毁,main方法中num的值保持5。
引用数据类型
底层原理:
引用数据类型(自定义类 / 数组 / 集合 / String 等)的实参存储逻辑:
栈内存:存储对象的引用地址(指向堆内存中的实际对象);
堆内存:存储对象的属性 / 数据。
传参时,JVM 复制的是栈中的引用地址副本给形参:
形参和实参的地址副本指向同一个堆对象;
方法内通过地址副本修改堆对象的属性:会影响原对象(因为指向同一个堆内存);
方法内修改形参的地址指向(比如
new新对象):仅修改副本的地址,原实参的地址不变,原对象不受影响。
场景 1:修改堆对象的属性(影响原对象):
// 自定义引用类型
class Person {
String name;
int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
public class RefTypeParam1 {
// 方法:修改对象的age属性
public static void changePersonAttr(Person p) {
p.age = 20; // 通过地址副本修改堆对象属性
System.out.println("方法内p的age:" + p.age); // 输出:20
}
public static void main(String[] args) {
// 实参person:栈存地址0x003,堆存{name:"张三", age:18}
Person person = new Person("张三", 18);
changePersonAttr(person); // 传递地址副本0x003给形参p
System.out.println("方法外person的age:" + person.age); // 输出:20(原对象被修改)
}
}内存执行流程:
main中创建Person对象:栈中person存地址0x003,堆中0x003地址存储{name:"张三", age:18};- 调用方法时,形参
p得到地址副本0x003,此时person和p都指向堆中同一个对象; - 方法内
p.age=20:直接修改堆中0x003地址的age属性,原person的age同步变化; - 方法结束后,
p出栈,person仍指向0x003,age为20。
场景 2:修改引用地址的指向(不影响原对象):
public class RefTypeParam2 {
// 方法:修改形参p的地址指向
public static void changePersonRef(Person p) {
// 形参p指向新的堆对象(地址0x004)
p = new Person("李四", 25);
System.out.println("方法内p的name:" + p.name); // 输出:李四
}
public static void main(String[] args) {
Person person = new Person("张三", 18); // 指向0x003
changePersonRef(person); // 传递0x003给p
System.out.println("方法外person的name:" + person.name); // 输出:张三(原对象未变)
}
}内存执行流程:
main中person指向堆地址0x003(张三,18);- 方法调用时,
p得到0x003的副本,此时p和person都指向0x003; - 方法内
p = new Person(...):p的地址改为0x004(李四,25),但person仍指向0x003; - 方法结束后,
p出栈,person的地址和指向的对象均未变化。
不可变类型
String、Integer、Double等包装类属于不可变类型:对象一旦创建,内部属性(如String的value数组、Integer的value)无法修改,所有 “修改” 操作本质是创建新对象。
String 传参示例:
public class StringParam {
public static void changeString(String s) {
s = "李四"; // 不是修改原对象,而是创建新String对象(地址0x005)
System.out.println("方法内s的值:" + s); // 输出:李四
}
public static void main(String[] args) {
String str = "张三"; // 栈中str指向常量池地址0x004("张三")
changeString(str); // 传递0x004给s
System.out.println("方法外str的值:" + str); // 输出:张三(原对象未变)
}
}Integer 包装类传参示例:
public class IntegerParam {
public static void changeInteger(Integer i) {
i = 20; // 自动装箱,创建新Integer对象(地址0x006)
System.out.println("方法内i的值:" + i); // 输出:20
}
public static void main(String[] args) {
Integer num = 10; // 指向常量池地址0x005(10)
changeInteger(num);
System.out.println("方法外num的值:" + num); // 输出:10
}
}核心原因:
不可变类型的 “修改” 本质是重新赋值,即让形参指向新对象,但原实参的地址仍指向旧对象,因此无法影响原实参。
递归
概述
定义
递归(Recursion):是指方法在执行过程中直接或间接调用自身的编程方式,用于解决满足以下条件的问题:
问题可拆解为规模更小的同类子问题(子问题与原问题逻辑一致);
存在终止条件(当问题规模缩小到一定程度时,无需递归直接返回结果)。
递归能解决的问题:
- 数学问题:阶乘、斐波那契数列、汉诺塔、八皇后、球和篮子问题。
- 算法问题:快排、归并排序、二分查找、分治算法。
- 栈相关问题:迷宫路径查找。
本质:分治思想
递归的核心:是 “分而治之”:将大问题拆解为多个小问题,逐个解决小问题后,合并结果得到原问题的解。例如:
计算 n!(n 的阶乘):n! = n * (n-1)!,子问题是 (n-1)!,终止条件是 0! = 1;
遍历二叉树:先递归遍历左子树,再处理当前节点,最后递归遍历右子树,终止条件是 “节点为 null”。
工作原理
Java 中方法调用通过方法栈(栈帧) 实现,递归调用的本质是栈帧的 “压栈” 与 “出栈” 过程:
压栈(递归调用):每次调用方法时,JVM 会创建新的栈帧(存储方法的参数、局部变量、返回地址),并压入栈顶;
出栈(递归返回):当方法执行到终止条件或完成逻辑时,栈帧弹出,返回结果给上一层调用者,直到栈为空(回到最初的调用方法)。
示例:阶乘递归的栈帧变化
以 factorial(3) 为例,逐步解析栈的变化:
// 阶乘递归方法
public static int factorial(int n) {
if (n == 0) return 1; // 终止条件
return n * factorial(n-1); // 递归调用
}内存流程图与栈帧变化:

优缺点
优点:
代码简洁:无需手动管理循环逻辑,直接映射问题的数学模型或逻辑结构(如树遍历);
逻辑清晰:符合人类 “分治” 思维,便于理解和维护;
适用场景广:天然适配树、图、分治算法(如归并排序、快速排序)。
缺点:
栈溢出风险:递归深度过大时,方法栈会不断压栈,超出 JVM 栈容量(默认约 1MB),抛出 StackOverflowError;
重复计算:如原生斐波那契数列递归,会重复计算大量子问题(如 fib(5) 需计算 fib(4) 和 fib(3),fib(4) 又需计算 fib(3),导致时间复杂度 O (2ⁿ));
效率较低:方法调用有额外的栈帧开销(压栈、出栈、参数传递),比迭代效率低。
注意事项
必须明确终止条件:且终止条件需能被触发(如 n 逐步递减到 0,而非递增);
控制递归深度:避免深度过大(如超过 1000 层),优先用迭代或记忆化搜索;
避免重复计算:复杂递归问题优先考虑缓存(记忆化搜索);
处理非法输入:如阶乘的 n<0、斐波那契的 n,需抛出异常或返回默认值;
警惕对象引用共享:递归方法中若使用全局变量或共享对象,需注意线程安全和状态污染(优先用局部变量传递状态)。
核心要素
递归的正确执行依赖两个不可缺少的要素,缺少任何一个都会导致程序异常(死循环、栈溢出):
- 终止条件:当问题规模缩小到某个临界值时,直接返回结果,不再递归调用。
- 递归步骤:将原问题拆解为规模更小、逻辑相同的子问题,通过调用自身解决子问题。
终止条件
终止条件:当问题规模缩小到某个临界值时,直接返回结果,不再递归调用;
作用:阻止方法无限调用自身,避免栈溢出(StackOverflowError);
示例:阶乘的 n==0、斐波那契数列的 n==1 || n==2、树遍历的 node==null。
错误示例(无终止条件):
// 无终止条件,会无限递归导致栈溢出
public static int badRecursion(int n) {
return n + badRecursion(n-1);
}递归步骤
递归步骤:将原问题拆解为规模更小、逻辑相同的子问题,通过调用自身解决子问题;
要求:子问题必须 “逐步逼近” 终止条件(否则仍会无限递归);
示例:n! = n * (n-1)!(子问题 (n-1)! 的规模比 n! 小 1,逐步逼近 n=0)。
常见应用场景
递归适用于 “问题可分治、结构有自相似性” 的场景,以下是 Java 中最典型的应用:
数学问题
打印问题
/** 打印问题 */
public void test(int n) {
if (n > 2) {
test(n - 1); // 递归调用
}
System.out.println("n=" + n);
}
test(4); // 输出:n=2,n=3,n=4内存流程图:

阶乘计算
public class FactorialDemo {
public static int factorial(int n) {
// 终止条件:n<0抛异常,n=0返回1
if (n < 0) throw new IllegalArgumentException("n不能为负数");
if (n == 0) return 1;
// 递推关系
return n * factorial(n - 1);
}
public static void main(String[] args) {
System.out.println(factorial(5)); // 120
}
}内存流程图:

斐波那契数列
/**
* 斐波那契数列:F(1)=1, F(2)=1, F(n)=F(n-1)+F(n-2)
* 注意:原生递归存在大量重复计算,下文会优化
*/
public static int fibonacci(int n) {
if (n <= 0) throw new IllegalArgumentException("n 必须大于 0");
// 终止条件
if (n == 1 || n == 2) return 1;
// 递归步骤
return fibonacci(n - 1) + fibonacci(n - 2);
}
fibonacci(5) // 1 1 2 3 5内存流程图:
.png)
数据结构操作
二叉树的前序遍历
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int val) { this.val = val; }
}
/**
* 二叉树前序遍历:递归版
*/
public static void preOrderTraversal(TreeNode node) {
// 终止条件:节点为 null 时返回
if (node == null) return;
// 处理当前节点
System.out.print(node.val + " ");
// 递归遍历左子树
preOrderTraversal(node.left);
// 递归遍历右子树
preOrderTraversal(node.right);
}
// 调用:preOrderTraversal(root) → 输出根、左子树、右子树节点值链表反转
class ListNode {
int val;
ListNode next;
ListNode(int val) { this.val = val; }
}
/**
* 链表反转:递归版
*/
public static ListNode reverseList(ListNode head) {
// 终止条件:空链表或只有一个节点,直接返回自身
if (head == null || head.next == null) return head;
// 递归反转后续链表,得到反转后的头节点
ListNode newHead = reverseList(head.next);
// 调整当前节点与下一个节点的指向
head.next.next = head;
head.next = null;
// 返回反转后的头节点
return newHead;
}文件目录遍历
import java.io.File;
/**
* 递归遍历指定目录下的所有文件(包括子目录)
*/
public static void traverseDirectory(File dir) {
// 终止条件:不是目录或目录不存在
if (!dir.exists() || !dir.isDirectory()) return;
// 获取目录下的所有文件/子目录
File[] files = dir.listFiles();
if (files == null) return; // 防止权限问题导致的 null
// 遍历每个文件/子目录
for (File file : files) {
if (file.isDirectory()) {
// 子目录:递归遍历
traverseDirectory(file);
} else {
// 文件:输出路径
System.out.println("文件路径:" + file.getAbsolutePath());
}
}
}
// 调用:traverseDirectory(new File("D:/test")) → 遍历 D:/test 下所有文件经典递归问题
猴子吃桃
问题:猴子第一天吃了桃子的一半多 1 个,以后每天吃剩下的一半多 1 个,第 5 天剩 1 个桃子。求最初桃子总数。
思路(逆推):
- 第 5 天:1 个
- 第 4 天:(1+1)×2=4 个
- 第 3 天:(4+1)×2=10 个
- 规律:前一天桃子数 = (后一天桃子数 + 1)×2
public class RecursionExercise01 {
public int peach(int day) {
if (day == 5) {
return 1; // 终止条件(第5天剩1个)
} else if (day >= 1 && day <= 9) {
return (peach(day + 1) + 1) * 2; // 递归调用(逆推)
} else {
System.out.println("day必须在[1,10]之间");
return -1;
}
}
public static void main(String[] args) {
int day = 1;
int peachNum = peach(day);
if (peachNum != -1) {
System.out.println("第" + day + "天有" + peachNum + "个桃子"); // 输出1534
}
}
}内存流程图:
.jpg)
迷宫问题
问题:用二维数组表示迷宫,0 表示可走,1 表示障碍物,2 表示已走通,3 表示死路。使用递归回溯找通路。
public class MazeRecursion {
// 1. 构建迷宫:8行7列,1=墙,0=通路
public static int[][] createMaze() {
int[][] maze = new int[8][7];
// 初始化所有位置为0(通路)
for (int i = 0; i < 8; i++) {
for (int j = 0; j < 7; j++) {
maze[i][j] = 0;
}
}
// 构建上下边界(墙)
for (int j = 0; j < 7; j++) {
maze[0][j] = 1; // 第一行
maze[7][j] = 1; // 第八行
}
// 构建左右边界(墙)
for (int i = 0; i < 8; i++) {
maze[i][0] = 1; // 第一列
maze[i][6] = 1; // 第七列
}
// 构建内部墙
maze[3][1] = 1;
maze[3][2] = 1;
return maze;
}
// 2. 打印迷宫:直观展示路径
public static void printMaze(int[][] maze) {
for (int i = 0; i < maze.length; i++) {
for (int j = 0; j < maze[i].length; j++) {
System.out.print(maze[i][j] + " ");
}
System.out.println();
}
}
/**
* 3. 递归找迷宫路径
* @param maze 迷宫数组
* @param i 当前行
* @param j 当前列
* @param endI 终点行
* @param endJ 终点列
* @return 是否找到通路
*/
public static boolean findPath(int[][] maze, int i, int j, int endI, int endJ) {
// 递归终止条件:到达终点
if (maze[endI][endJ] == 2) {
return true;
}
// 合法性检查:当前位置在范围内,且是未走的通路(0)
if (i >= 0 && i < maze.length && j >= 0 && j < maze[0].length && maze[i][j] == 0) {
// 标记当前位置为已走的通路(2)
maze[i][j] = 2;
// 按优先级尝试方向:下 → 右 → 上 → 左
// 1. 尝试向下走
if (findPath(maze, i + 1, j, endI, endJ)) {
return true;
}
// 2. 向下走不通,尝试向右走
else if (findPath(maze, i, j + 1, endI, endJ)) {
return true;
}
// 3. 向右走不通,尝试向上走
else if (findPath(maze, i - 1, j, endI, endJ)) {
return true;
}
// 4. 向上走不通,尝试向左走
else if (findPath(maze, i, j - 1, endI, endJ)) {
return true;
}
// 5. 所有方向都走不通,标记为死路(3),回溯
else {
maze[i][j] = 3;
return false;
}
}
// 当前位置不合法(墙/死路/越界),返回false
return false;
}
// 主方法测试
public static void main(String[] args) {
// 1. 创建迷宫
int[][] maze = createMaze();
System.out.println("=== 初始迷宫 ===");
printMaze(maze);
// 2. 递归找路径:起点(1,1),终点(6,5)
boolean hasPath = findPath(maze, 1, 1, 6, 5);
// 3. 输出结果
System.out.println("\n=== 路径查找结果 ===");
if (hasPath) {
System.out.println("找到通路!迷宫路径如下:");
printMaze(maze);
} else {
System.out.println("未找到通路!");
}
}
}内存流程图:
.jpg)
汉诺塔
规则:3 根柱子(A、B、C),n 个圆盘从 A 移到 C,每次只能移 1 个,小圆盘不能放大圆盘下。
public class HanoiTower {
// num:要移动的圆盘数;a:源柱子;b:辅助柱子;c:目标柱子
public void move(int num, char a, char b, char c) {
if (num == 1) {
System.out.println(a + "->" + c); // 1个圆盘直接移
} else {
// 如果有多个圆盘,可以看成2个圆盘:最下面的和上面的所有盘
// 1. 把num-1个圆盘从A移到B,借助C
move(num - 1, a, c, b);
// 2. 把最下面的1个圆盘从A移到C
System.out.println(a + "->" + c);
// 3. 把num-1个圆盘从B移到C,借助A
move(num - 1, b, a, c);
}
}
public static void main(String[] args) {
move(3, 'A', 'B', 'C'); // 3个圆盘,从A移到C,借助B
}
}内存流程图:
.jpg)
八皇后
问题:在 8×8 棋盘上摆放 8 个皇后,任意两个皇后不能同行、同列、同斜线,求摆法总数。
思路(回溯法):
- 用一维数组
arr[8]表示,arr[i]表示第 i 行皇后的列号。 - 逐行摆放皇后,每放一个皇后,检查是否与已摆放的皇后冲突。
- 冲突则回溯,更换列号;无冲突则继续摆下一行,直到 8 个皇后全部摆放完毕(找到一个解)。
数组求和
/**
* 递归求和:数组 arr 从索引 start 到末尾的和
*/
public static int sumArray(int[] arr, int start) {
// 终止条件:start 超出数组长度,和为 0
if (start >= arr.length) return 0;
// 递归步骤:当前元素 + 后续元素的和
return arr[start] + sumArray(arr, start + 1);
}
sumArray(new int[]{1,2,3,4}, 0) // 调用:→ 10优化方案
记忆化搜索
针对重复计算问题,用数组、HashMap 等缓存已计算的子问题结果,避免重复计算。
优化斐波那契数列(记忆化搜索):
import java.util.HashMap;
import java.util.Map;
public class FibonacciOptimized {
// 缓存已计算的结果:key = n,value = F(n)
private static Map cache = new HashMap
public static int fib(int n) {
if (n ) throw new IllegalArgumentException("n 必须大于 0");
// 终止条件
if (n == 1 || n == 2) return 1;
// 检查缓存:已计算则直接返回
if (cache.containsKey(n)) {
return cache.get(n);
}
// 递归计算并缓存结果
int result = fib(n - 1) + fib(n - 2);
cache.put(n, result);
return result;
}
// 时间复杂度优化为 O(n),空间复杂度 O(n)(缓存 + 栈)
}迭代改写
将递归逻辑改为循环,手动管理计算过程,避免栈溢出和方法调用开销。
迭代改写斐波那契数列:
public static int fibIterative(int n) {
if (n throw new IllegalArgumentException("n 必须大于 0");
if (n == 1 || n == 2) return 1;
int a = 1, b = 1; // a = F(n-2), b = F(n-1)
int result = 0;
for (int i = 3; i ; i++) {
result = a + b;
a = b; // 更新 F(n-2) 为原 F(n-1)
b = result; // 更新 F(n-1) 为当前结果
}
return result;
}
// 时间复杂度 O(n),空间复杂度 O(1)(无栈开销,无缓存)尾递归
尾递归是指递归调用是方法的最后一个操作,无后续计算(如 return fibTail(n-1, a, b))。理论上,编译器可优化尾递归,复用栈帧(无需压栈新帧),避免栈溢出。
但 Java 编译器不支持尾递归优化(JVM 未实现),即使写尾递归,仍会压栈导致栈溢出。示例如下(仅作理论参考):
/**
* 尾递归版斐波那契数列(Java 不优化,仍可能栈溢出)
* @param n 目标项
* @param a F(n-2)
* @param b F(n-1)
*/
public static int fibTail(int n, int a, int b) {
if (n == 1) return a;
if (n == 2) return b;
// 递归调用是最后一个操作(尾递归)
return fibTail(n - 1, b, a + b);
}
// 调用:fibTail(5, 1, 1) → 5增加 JVM 栈容量
通过 JVM 参数 -Xss 增大栈容量(如 -Xss2m 表示栈容量为 2MB),可缓解栈溢出,但不根本解决问题(递归深度过大仍会溢出),不推荐依赖。
方法重载
概述
方法重载(Method Overload):在同一个类中,存在多个方法满足以下条件:
- 方法名称完全相同;
- 参数列表必须不同(参数个数、参数类型、参数顺序,三者满足其一即可);
- 与参数名、返回值类型、访问修饰符、抛出异常列表无关。
本质:编译时静态绑定:
Java 编译器在编译阶段(而非运行阶段),会根据调用方法时传入的实参的静态类型(声明类型),匹配对应的重载方法并确定调用目标,这个过程称为 “静态绑定”。
方法重写(Override):是运行时动态绑定,根据对象的实际类型确定调用方法。
重载的核心价值:
- 语义统一:同一类操作(如 “加法”“打印”)用同一个方法名,符合 “见名知意” 的编码习惯;
- 简化调用:调用者无需记忆不同功能的方法名,仅需传入不同参数即可;
- 适配多场景:支持同一逻辑适配不同输入参数(如计算不同个数的数值和、处理不同类型的数据源)。
快速入门
public class OverLoad01 {
// 1. 两个int的和
public int calculate(int n1, int n2) {
System.out.println("调用:两个int的和");
return n1 + n2;
}
// 2. int+double的和
public double calculate(int n1, double n2) {
System.out.println("调用:int+double的和");
return n1 + n2;
}
// 3. double+int的和
public double calculate(double n1, int n2) {
System.out.println("调用:double+int的和");
return n1 + n2;
}
// 4. 三个int的和
public int calculate(int n1, int n2, int n3) {
System.out.println("调用:三个int的和");
return n1 + n2 + n3;
}
public static void main(String[] args) {
System.out.println(calculate(1, 2)); // 调用两个int参数的方法
System.out.println(calculate(1, 2.1)); // 调用int+double的方法
System.out.println(calculate(1.1, 2)); // 调用double+int的方法
System.out.println(calculate(1, 2, 3)); // 调用三个int参数的方法
}
}编译器匹配优先级
编译器在匹配重载方法时,遵循 “精确匹配 → 自动类型转换匹配 → 可变参数匹配” 的优先级,逐级匹配,直到找到唯一匹配的方法;若匹配到多个或无匹配,均会报错。
优先级 1:精确匹配(最优先)
实参类型与形参类型完全一致,直接匹配:
javapublic class OverloadMatch { public static void print(int a) { System.out.println("int类型:" + a); } public static void print(double a) { System.out.println("double类型:" + a); } public static void main(String[] args) { print(10); // 精确匹配print(int) → int类型:10 print(10.5); // 精确匹配print(double) → double类型:10.5 } }优先级 2:自动类型转换匹配
若无精确匹配,编译器会尝试自动向上转型(如
int → long → float → double、char → int)匹配:注意:若存在多个可转换的匹配(如同时有
long和float重载),编译器会报错(模糊的方法调用)。javapublic class OverloadMatch { public static void print(long a) { System.out.println("long类型:" + a); } public static void print(double a) { System.out.println("double类型:" + a); } public static void main(String[] args) { print(10); // int无精确匹配,自动转long → long类型:10 print('a');// char无精确匹配,自动转int→再转long → long类型:97 } }优先级 3:可变参数匹配(最低)
仅当精确匹配、自动转换匹配均失败时,才会匹配可变参数(...)的重载方法:
public class OverloadMatch {
public static void print(int... nums) {
System.out.println("可变参数:" + Arrays.toString(nums));
}
public static void main(String[] args) {
print(10, 20); // 无精确匹配,匹配可变参数 → 可变参数:[10, 20]
}
}练习
练习 1:判断题
与 `void show(int a, char b, double c)` 构成重载的有(b c d e):
- a) void show(int x, char y, double z){} // 否(形参列表完全相同)
- b) int show(int a, double c, char b){} // 是(形参顺序不同)
- c) void show(int a, double c, char b){} // 是(形参顺序不同)
- d) boolean show(int c, char b){} // 是(形参个数不同)
- e) void show(double c){} // 是(形参个数、类型不同)
- f) double show(int x, char y, double z){} // 否(形参列表完全相同)
- g) void shows(){} // 否(方法名不同)练习 2:重载方法实现
定义
Methods类的 3 个重载方法m:m(int n):输出 n 的平方。m(int n1, int n2):输出 n1×n2 的结果。m(String str):输出字符串。
javaclass Methods { // 1. 定义 m 方法的重载 public void m(int n) { System.out.println("平方=" + (n * n)); }public void m(int n1, int n2) { System.out.println("相乘=" + (n1 * n2)); } public void m(String str) { System.out.println("传入的str=" + str); } } public class OverLoadExercise { public static void main(String[] args) { Methods method = new Methods(); // 2. 测试 m 方法 method.m(10); // 输出:平方=100 method.m(10, 20); // 输出:相乘=200 method.m("韩顺平教育hello"); // 输出:传入的str=韩顺平教育hello } } 定义 3 个重载方法
max:max(int n1, int n2):返回两个 int 的最大值。max(double n1, double n2):返回两个 double 的最大值。max(double n1, double n2, double n3):返回三个 double 的最大值。
javaclass Methods { // 1. 定义 max 方法的重载 public int max(int n1, int n2) { return n1 > n2 ? n1 : n2; }public double max(double n1, double n2) { return n1 > n2 ? n1 : n2; } public double max(double n1, double n2, double n3) { double max1 = n1 > n2 ? n1 : n2; return max1 > n3 ? max1 : n3; } } public class OverLoadExercise { public static void main(String[] args) { Methods method = new Methods(); // 2. 测试 max 方法 System.out.println(method.max(10, 24)); // 24 System.out.println(method.max(10.0, 21.4)); // 21.4 System.out.println(method.max(10.0, 1.4, 30)); // 30.0 } }
可变参数
概述
可变参数(Variable Arguments,Varargs):是方法参数的一种特殊形式,通过 类型名... 参数名 声明,允许调用方法时传入 0 个、1 个或多个同类型参数,编译器会自动将这些参数封装为一个数组,方法内部可直接将可变参数当作数组处理。
核心本质:数组的 “语法糖”
可变参数并非新的参数类型,而是 JDK 为简化数组参数调用设计的语法糖 —— 编译阶段,编译器会将 类型... 参数名 转换为 类型[] 参数名,方法调用时传入的零散参数会被自动打包为数组。
示例:验证数组本质
编译后,sum(int... nums) 会被转换为 sum(int[] nums),上述调用中,sum(1,2,3) 会被编译器自动转换为 sum(new int[]{1,2,3})。
public class VarargsDemo {
// 可变参数方法
public static int sum(int... nums) {
int total = 0;
// 可变参数本质是数组,可遍历
for (int num : nums) {
total += num;
}
return total;
}
public static void main(String[] args) {
// 调用方式1:传0个参数(数组长度0)
System.out.println(sum()); // 0
// 调用方式2:传1个参数
System.out.println(sum(1)); // 1
// 调用方式3:传多个参数
System.out.println(sum(1, 2, 3)); // 6
// 调用方式4:直接传数组(编译器兼容)
int[] arr = {4, 5, 6};
System.out.println(sum(arr)); // 15
}
}语法格式
可变参数声明在方法参数列表中,格式为:
[修饰符] 返回值类型 方法名([固定参数列表], 类型... 可变参数名) {
// 方法体:可变参数当作数组处理
}快速入门
实现计算任意个数的 int 类型之和。
public class VarParameter01 {
// 可变参数sum:接收任意个数的int,使用时可以当做数组来用
public int sum(int... nums) {
int res = 0;
for (int i = 0; i < nums.length; i++) {
res += nums[i];
}
return res;
}
public static void main(String[] args) {
System.out.println(sum(1, 5, 100)); // 106(3个数之和)
System.out.println(sum(1, 19)); // 20(2个数之和)
System.out.println(sum()); // 0(0个数之和)
}
}核心使用规则(必守)
核心使用规则(必守):
规则 1:可变参数必须是参数列表的最后一个
可变参数接收 “任意数量” 的参数,若后面还有其他参数,编译器无法区分可变参数的结束位置,因此编译报错:
javapublic class VarargsRule1 { // ✅ 正确:可变参数在最后 public static void print(String prefix, int... nums) {} // ❌ 错误:可变参数后有其他参数(编译报错:Varargs parameter must be the last parameter) public static void print(int... nums, String suffix) {} }规则 2:一个方法只能有一个可变参数
同理,多个可变参数会导致编译器无法拆分参数边界,编译报错:
javapublic class VarargsRule2 { // ❌ 错误:一个方法多个可变参数(编译报错:Variable arity parameter must be the last parameter) public static void sum(int... nums1, double... nums2) {} }规则 3:可变参数可接收 0 个参数
调用时不传可变参数,编译器会传入一个空数组(而非
null),方法内部遍历不会报空指针:javapublic class VarargsRule3 { public static void print(int... nums) { System.out.println("数组长度:" + nums.length); // 0个参数时输出0 for (int num : nums) { // 遍历空数组,无异常 System.out.println(num); } } public static void main(String[] args) { print(); // 数组长度:0 } }规则 4:可变参数支持基本类型和引用类型
- 基本类型可变参数:
int...、double...等; - 引用类型可变参数:
String...、Person...等。
javaclass Person { private String name; public Person(String name) { this.name = name; } } public class VarargsType { // 引用类型可变参数 public static void printPersons(Person... persons) { for (Person p : persons) { System.out.println(p.name); } } public static void main(String[] args) { printPersons(new Person("张三"), new Person("李四")); } }- 基本类型可变参数:
规则 5:可直接传递数组给可变参数
编译器兼容数组参数,无需手动拆分,等价于传入多个零散参数:
javapublic class VarargsArray { public static int sum(int... nums) { int total = 0; for (int num : nums) total += num; return total; } public static void main(String[] args) { int[] arr = {1,2,3}; // 直接传数组,等价于sum(1,2,3) System.out.println(sum(arr)); // 6 } }
底层实现原理
JVM 本身不支持可变参数,其实现完全依赖编译器的 “语法糖转换”,核心步骤:
- 编译方法时:将
类型... 参数名替换为类型[] 参数名,方法签名变为方法名(类型[]); - 编译方法调用时:
- 若传入零散参数(如
sum(1,2,3)),自动打包为数组new int[]{1,2,3}; - 若传入数组(如
sum(arr)),直接传递数组引用,不做额外处理; - 若传入 0 个参数,自动创建空数组
new int[0]。
- 若传入零散参数(如
反编译验证(使用 javap -c 命令):
编译 sum(int... nums) 后,字节码中方法签名为 sum([I)I([I 表示 int 数组),与 sum(int[] nums) 完全一致。
练习
封装一个可变参数方法showScore,接收姓名和任意门课成绩,返回“姓名+课程数+总分”。
public class VarParameterExercise {
public String showScore(String name, double... scores) {
double totalScore = 0;
for (int i = 0; i < scores.length; i++) {
totalScore += scores[i];
}
return name + " 有" + scores.length + "门课,总分为:" + totalScore;
}
public static void main(String[] args) {
System.out.println(showScore("milan", 90.1, 80.0));
System.out.println(showScore("terry", 90.1, 80.0, 10, 30.5, 70));
}
}作用域
概述
作用域(Scope):是 Java 中程序元素(变量、方法、类、接口等)的可见范围和生命周期的集合,核心作用是控制元素的访问权限和内存管理。其中,变量的作用域是最核心、最易混淆的部分(方法 / 类的作用域主要由访问修饰符决定)。
作用域的关键维度:
- 可见性(Visibility):程序的哪个部分能访问该元素(如 “方法内的变量仅方法内可见”)
- 生命周期(Lifecycle):元素的创建 / 销毁时机(如 “局部变量随方法调用创建,调用结束销毁”)
作用域分类
作用域的核心分类(按范围从小到大):
块级作用域 < 方法/构造器作用域 < 类级作用域(成员变量) < 包级作用域 < 全局作用域(public类/方法)块级作用域
块级作用域(Block Scope):在代码块({} 包裹的区域)内声明的变量,包括:
- 条件块(
if/else、switch); - 循环块(
for、while、do-while); - 同步块(
synchronized); - 普通代码块(直接用
{}包裹的代码)。
核心特性:
- 生命周期:进入块时在栈内存创建,退出块时立即销毁(栈帧弹出)
- 可见性:仅在当前块及嵌套的子块内可见,块外无法访问
- 默认值:无默认值,必须显式初始化后才能使用(否则编译报错)
- 命名规则:同一块内不能声明同名变量;块嵌套时,内层变量可覆盖外层同名变量(就近原则)
示例:
普通块作用域
javapublic class BlockScopeDemo { public static void main(String[] args) { // 外层块 { int a = 10; // 块级变量,作用域:外层块 System.out.println(a); // 合法:10 // 嵌套块 { int b = 20; // 作用域:嵌套块 System.out.println(a + b); // 合法:30(内层可访问外层) int a = 30; // 合法:内层覆盖外层同名变量(就近原则) System.out.println(a); // 30(访问内层a) } // System.out.println(b); // 错误:b仅在嵌套块内可见 } // System.out.println(a); // 错误:a仅在外层块内可见 } }循环 / 条件块作用域
javapublic class BlockScopeDemo2 { public static void main(String[] args) { // for循环块:i的作用域仅在循环内 for (int i = 0; i < 3; i++) { System.out.println(i); // 合法 } // System.out.println(i); // 错误:i超出作用域 // if块:flag的作用域仅在if内 if (true) { boolean flag = true; System.out.println(flag); // 合法 } // System.out.println(flag); // 错误:flag超出作用域 } }
方法/构造器作用域
方法 / 构造器作用域(Method/Constructor Scope):在方法 / 构造器内声明的局部变量(包括方法的形参),是块级作用域的 “超集”(方法体本身就是一个大代码块)。
核心特性:
- 生命周期:方法 / 构造器调用时创建(栈帧压入),执行完毕后销毁(栈帧弹出)
- 可见性:整个方法 / 构造器内可见(包括内部嵌套块),方法外无法访问
- 默认值:无默认值,必须显式初始化(形参由调用者传值初始化)
- 命名规则:方法内局部变量不能与形参同名;方法内嵌套块可覆盖方法级变量
示例:方法局部变量 + 形参
public class MethodScopeDemo {
public static int add(int a, int b) { // 1. 方法形参(属于方法作用域)
int sum = a + b; // 2. 方法级局部变量
if (sum > 10) {
int temp = sum * 2; // 嵌套块变量,仅if内可见
sum = temp;
}
// System.out.println(temp); // 错误:temp超出作用域
return sum;
}
public static void main(String[] args) {
int result = add(3, 5);
// System.out.println(a); // 错误:形参a超出方法作用域
// System.out.println(sum); // 错误:sum超出方法作用域
}
}类级作用域(成员变量)
类级作用域(成员变量,属性):在类内、方法 / 块外声明的变量(类的成员),分为两类:
- 实例变量(非静态)
- 静态变量(类变量)
实例变量
实例变量(Instance Variable)特性:
- 修饰符:无
static修饰,可加private/public/protected等访问修饰符 - 生命周期:对象创建时在堆内存分配,对象被 GC 回收时销毁
- 可见性:整个类内可见;外部需通过对象实例访问(
obj.var),受访问修饰符限制 - 默认值:有默认值(同数组规则:int→0,String→null,boolean→false 等)
- 线程安全:非线程安全(每个对象有独立副本)
示例:实例变量
public class ClassScopeDemo {
// 1. 实例变量(对象级,每个对象独立)
private String name;
private int age; // 默认值0
public ClassScopeDemo(String name, int age) {
this.name = name;
this.age = age;
}
public void show() {
// 2. 类内可直接访问成员变量
System.out.println("姓名:" + name + ",年龄:" + age);
}
public static void main(String[] args) {
// 3. 类外:必须通过对象访问实例变量
ClassScopeDemo obj1 = new ClassScopeDemo("张三", 20);
ClassScopeDemo obj2 = new ClassScopeDemo("李四", 25);
obj1.show(); // 姓名:张三,年龄:20
obj2.show(); // 姓名:李四,年龄:25
}
}静态变量
静态变量(Class Variable)特性:
- 修饰符:
static修饰,可加访问修饰符 - 生命周期:类加载时在方法区分配,类卸载(JVM 退出)时销毁
- 可见性:整个类内可见;外部可通过类名 / 对象实例访问(
Class.var),受访问修饰符限制 - 默认值:有默认值(同数组规则:int→0,String→null,boolean→false 等)
- 线程安全:线程不安全(所有对象共享同一个副本)
示例:静态变量
public class ClassScopeDemo {
// 1. 静态变量(类级,所有对象共享)
private static String className = "Java基础";
public void show() {
// 2. 类内可直接访问成员变量(与实例变量相同)
System.out.println("类名:" + className);
}
public static void main(String[] args) {
// 3. 类外静态变量:通过类名访问(推荐)
System.out.println(ClassScopeDemo.className); // Java基础
// 4. 静态变量共享:修改后所有对象可见
ClassScopeDemo.className = "Java进阶";
obj1.show(); // 类名:Java进阶
obj2.show(); // 类名:Java进阶
}
}异常参数作用域
异常参数作用域(Catch Scope):catch 块中声明的异常参数(如 catch (Exception e)),属于特殊的块级作用域。
核心特性:
- 生命周期:
catch块执行时创建,执行完毕后销毁 - 可见性:仅在当前
catch块内可见 - 命名规则:同一
try-catch的多个catch块可声明同名异常参数(不同作用域)
示例:catch 块异常参数
public class CatchScopeDemo {
public static void main(String[] args) {
try {
int a = 1 / 0;
} catch (ArithmeticException e) {
// e仅在当前catch块内可见
System.out.println("算术异常:" + e.getMessage());
} catch (Exception e) {
// 不同catch块,同名e合法
System.out.println("通用异常:" + e.getMessage());
}
// System.out.println(e); // 错误:e超出catch作用域
}
}内部类作用域
内部类的作用域依赖其定义位置,分为:
- 成员内部类
- 局部内部类
- 匿名内部类
成员内部类
成员内部类:类内、方法外的内部类;
可见性:可访问外部类的所有成员(包括 private);
外部类访问内部类:需通过内部类实例(Outer.Inner inner = new Outer().new Inner())。
示例:成员内部类
public class InnerClassScopeDemo {
private String outerVar = "外部类变量";
// 1. 成员内部类
class MemberInner {
public void show() {
// 可访问外部类private成员
System.out.println(outerVar); // 外部类变量
}
}
public static void main(String[] args) {
InnerClassScopeDemo outer = new InnerClassScopeDemo();
// 2. 成员内部类:外部需通过实例访问
MemberInner memberInner = outer.new MemberInner();
memberInner.show();
}
}局部内部类
局部内部类:方法 / 块内的内部类;
可见性:仅在方法 / 块内可见;
访问规则:可访问外部类的成员,以及外部方法的 final/effectively final 局部变量(JDK 8+)。
示例:局部内部类
public class InnerClassScopeDemo {
private String outerVar = "外部类变量";
// 方法内局部内部类
public void testLocalInner() {
// effectively final变量(未重新赋值)
String localVar = "方法局部变量";
// 1. 局部内部类
class LocalInner {
public void show() {
System.out.println(outerVar); // 访问外部类成员
System.out.println(localVar); // 访问外部方法final变量
}
}
// 2. 局部内部类仅方法内可见
LocalInner inner = new LocalInner();
inner.show();
}
public static void main(String[] args) {
InnerClassScopeDemo outer = new InnerClassScopeDemo();
outer.testLocalInner();
}
}匿名内部类
匿名内部类:无类名的局部内部类;
作用域:与局部内部类一致,仅在定义的方法 / 块内有效;
访问规则:同局部内部类。
作用域规则总结
作用域规则总结:
默认值:
- 块级/方法/构造器:没有默认值,必须显式初始化后才能使用(否则编译报错)
- 类级(实例/静态):有默认值,规则与数组一致(int→0,String→null,boolean→false 等)
变量重名:
同一块内:不能声明同名变量;
javapublic void hi() { String address = "北京"; // String address = "上海"; // ❌ 错误(同一作用域局部变量重名) }块嵌套时:内层变量可覆盖外层同名变量(就近原则)
javaclass Person { String name = "jack"; public void say() { String name = "king"; // 局部变量与属性重名 System.out.println("say() name=" + name); // 就近原则,输出king } }
生命周期:
- 类级(实例/静态):伴随对象创建而创建,伴随对象销毁而销毁。
- 块级/方法/构造器:伴随代码块执行而创建,伴随代码块结束而销毁。
javaclass Person { // 1. 类级变量 name 是随着 Person 对象的创建而创建,随着 Person 对象的销毁而销毁(持续时间更久) String name = "jack"; public void say() { // 2. 局部变量 name 会随着 say() 调用而创建,也会随着 say() 调用结束而销毁 String name = "king"; } }访问范围:
- 类级(实例/静态):整个类内可见;外部需通过类名 / 对象实例访问(
Class.var/obj.var),受访问修饰符限制。 - 块级/方法/构造器:整个块 / 方法 / 构造器内可见(包括内部嵌套块),块 / 方法外无法访问。
javaclass Person { String name = "jack"; public void say() { String age = "king"; } public void hi() { System.out.println(age); // ❌ 2. 访问失败 } } class T { public void test() { Person p1 = new Person(); System.out.println(p1.name); // ✅ 1. 其他类通过对象访问属性,输出jack } }- 类级(实例/静态):整个类内可见;外部需通过类名 / 对象实例访问(
修饰符:
- 类级(实例/静态):可加
private/public/protected等访问修饰符。 - 块级/方法/构造器:不能加访问修饰符。
javaclass Person { // 1. 属性可以添加修饰符 public String name = "jack"; public void say() { // 2. 局部变量不可以添加修饰符 // public String age = "king"; } }- 类级(实例/静态):可加
构造方法
概述
构造方法(Constructor,构造器):是类中满足以下特征的特殊方法:
- 方法名必须与类名完全一致(包括大小写,如
Person类的构造方法名必须是Person); - 没有返回值类型(连
void都不能声明); - 不能被
static、final、abstract、native、synchronized等修饰(可被public/private/protected访问修饰符修饰); - 创建对象时由 JVM 自动调用,而非手动调用(仅能通过
this()/super()在构造方法内部调用其他构造)。
本质和作用:
构造方法的核心价值是保证对象创建时的初始化完整性:
- 初始化对象的成员变量(避免属性处于 “未初始化” 的默认值状态,如
int默认 0、String默认null); - 执行对象创建时的必要逻辑(如连接数据库、初始化集合、校验参数合法性);
- 控制对象的创建方式(如私有构造方法实现单例模式,禁止外部创建对象)。
语法格式
[访问修饰符] 类名([参数列表]) [throws 异常类型列表] {
// 构造方法体:初始化属性、执行初始化逻辑
}语法注意事项:
无返回值:构造方法不能声明返回值类型(包括
void),以下写法是错误的:java// 错误:不能写void public void Person() {} // 错误:不能写返回值类型 public int Person() { return 1; }方法名必须与类名一致:大小写错误会被识别为普通方法,而非构造方法:
javapublic class Person { // 错误:方法名是person(小写),类名是Person(大写),这是普通方法 public person() {} }
快速入门
// 定义Person类
public class Person {
// 成员变量
private String name;
private int age;
// 无参构造方法(自定义)
public Person() {
// 初始化默认值
this.name = "未知";
this.age = 0;
System.out.println("无参构造方法被调用");
}
// 有参构造方法(自定义)
public Person(String name, int age) {
// 初始化传入的属性值
this.name = name;
this.age = age;
System.out.println("有参构造方法被调用");
}
// 普通方法(对比构造方法)
public void showInfo() {
System.out.println("姓名:" + name + ",年龄:" + age);
}
public static void main(String[] args) {
// 创建对象时自动调用对应构造方法
Person p1 = new Person(); // 调用无参构造 → 无参构造方法被调用
p1.showInfo(); // 输出:姓名:未知,年龄:0
Person p2 = new Person("张三", 20); // 调用有参构造 → 有参构造方法被调用
p2.showInfo(); // 输出:姓名:张三,年龄:20
}
}核心特性
构造方法的核心特性:
自动调用:仅在 new 对象时触发
构造方法不能像普通方法一样通过
对象名.方法名()调用,只能在创建对象时由 JVM 自动执行:javapublic class Test { public static void main(String[] args) { Person p = new Person(); // 自动调用构造方法 // p.Person(); // 错误:构造方法不能手动调用 } }默认构造方法(隐式无参构造)
如果类中没有定义任何构造方法,JVM 会自动生成一个隐式的无参构造方法(默认构造):
访问修饰符与类的修饰符一致(类是
public,默认构造也是public;类是default,默认构造也是default);方法体为空,仅完成对象的默认初始化(成员变量赋默认值)。
示例:
javapublic class Person { private String name; private int age; // 未定义任何构造方法,JVM自动生成默认无参构造 // 等价于:public Person() {} public static void main(String[] args) { Person p = new Person(); // 调用默认无参构造 System.out.println(p.name); // null(默认值) System.out.println(p.age); // 0(默认值) } }默认构造的 “消失规则”
如果类中自定义了任意构造方法(无论有参 / 无参),JVM 不再自动生成默认无参构造:
javapublic class Person { private String name; private int age; // 自定义有参构造,默认无参构造消失 public Person(String name) { this.name = name; } public static void main(String[] args) { // Person p = new Person(); // 错误:找不到无参构造方法 Person p = new Person("张三"); // 正确:调用自定义有参构造 } }解决方案:若需要无参构造,需手动显式定义。
构造方法可重载(核心特性)
构造方法支持重载(与普通方法重载
规则一致):同一个类中,多个构造方法名相同(类名),参数列表不同(个数 / 类型 / 顺序)。重载的
目的是提供多种对象初始化方式(如无参初始化默认值、有参初始化指定值):javapublic class Person { private String name; private int age; private String gender; // 重载1:无参构造(初始化默认值) public Person() { this.name = "未知"; this.age = 0; this.gender = "未知"; } // 重载2:单参数构造(仅初始化姓名) public Person(String name) { this.name = name; this.age = 0; this.gender = "未知"; } // 重载3:三参数构造(初始化所有属性) public Person(String name, int age, String gender) { this.name = name; this.age = age; this.gender = gender; } public static void main(String[] args) { Person p1 = new Person(); // 调用无参构造 Person p2 = new Person("李四"); // 调用单参数构造 Person p3 = new Person("王五", 25, "男"); // 调用三参数构造 } }构造方法不能被继承
子类不会继承父类的构造方法,只能通过
super()调用父类构造方法。构造方法不能被 static 修饰
static 修饰的方法属于类,而构造方法是创建对象时调用的,依赖对象实例,因此冲突:
java// 错误:构造方法不能被static修饰 public static Person() {}
调用规则
构造方法内部可通过 this() 调用本类其他构造方法,或通过 super() 调用父类构造方法,核心规则:
this()/super()必须是构造方法体的第一条语句;- 不能同时在一个构造方法中调用
this()和super()(因为第一条语句只能有一个); this()用于重载构造之间的复用,super()用于初始化父类属性。
this
this ():调用本类其他构造方法
目的是复用构造方法的初始化逻辑,减少代码冗余:
public class Person {
private String name;
private int age;
// 无参构造:调用有参构造,传入默认值
public Person() {
this("未知", 0); // 调用本类的Person(String, int)构造,必须在第一行
System.out.println("无参构造执行");
}
// 有参构造:核心初始化逻辑
public Person(String name, int age) {
this.name = name;
this.age = age;
System.out.println("有参构造执行");
}
public static void main(String[] args) {
Person p = new Person();
// 输出顺序:
// 有参构造执行
// 无参构造执行
}
}super
super ():调用父类构造方法:
子类构造方法必须调用父类构造方法(显式 / 隐式),确保父类属性先初始化:
隐式调用:若子类构造方法中未写
super(),JVM 会自动在第一行插入super()(调用父类无参构造);java// 父类 class Parent { private String parentName; // 父类无参构造 public Parent() { this.parentName = "父类默认名称"; System.out.println("父类无参构造执行"); } } // 子类 class Child extends Parent { private String childName; // 子类无参构造:隐式调用super()(父类无参构造) public Child() { // 隐式super(),等价于:super(); this.childName = "子类默认名称"; System.out.println("子类无参构造执行"); } } // 测试 public class Test { public static void main(String[] args) { Child c1 = new Child(); // 输出: // 父类无参构造执行 // 子类无参构造执行 } }显式调用:手动指定
super(参数)调用父类有参构造,必须在构造方法第一行。java// 父类 class Parent { private String parentName; // 父类有参构造 public Parent(String parentName) { this.parentName = parentName; System.out.println("父类有参构造执行"); } } // 子类 class Child extends Parent { private String childName; // 子类有参构造:显式调用父类有参构造 public Child(String parentName, String childName) { super(parentName); // 调用父类有参构造,必须在第一行 this.childName = childName; System.out.println("子类有参构造执行"); } } // 测试 public class Test { public static void main(String[] args) { Child c2 = new Child("父类自定义名称", "子类自定义名称"); // 输出: // 父类有参构造执行 // 子类有参构造执行 } }调用规则注意点:若父类没有无参构造(仅自定义有参构造),子类构造必须显式调用父类有参构造,否则编译报错:
java// 父类:仅自定义有参构造,无默认无参构造 class Parent { public Parent(String name) {} } // 子类:编译错误,因为隐式super()会调用父类无参构造(不存在) class Child extends Parent { public Child() { // 错误:Implicit super constructor Parent() is undefined. Must explicitly invoke another constructor } // 正确:显式调用父类有参构造 public Child(String name) { super(name); } }
对象初始化顺序
创建对象时,初始化顺序为:
静态变量/静态代码块(类加载时执行,仅一次) → 实例变量/构造代码块(每次创建对象执行) → 构造方法(每次创建对象执行)
核心结论:
静态相关(变量 / 代码块)在类加载时执行,仅执行一次;
实例相关(变量 / 构造代码块 / 构造方法)在每次创建对象时执行;
构造代码块先于构造方法执行(构造代码块是 “所有构造方法的公共逻辑”)。
public class InitOrder {
// 静态变量
private static String staticVar = "静态变量初始化";
// 实例变量
private String instanceVar = "实例变量初始化";
// 1. 静态代码块:类加载时执行,仅执行一次
static {
System.out.println(staticVar);
System.out.println("静态代码块执行");
}
// 2. 构造代码块:每次创建对象时执行,先于构造方法执行
{
System.out.println(instanceVar);
System.out.println("构造代码块执行");
}
// 3. 构造方法:每次创建对象时执行
public InitOrder() {
System.out.println("构造方法执行");
}
public static void main(String[] args) {
System.out.println("=====创建第一个对象=====");
InitOrder obj1 = new InitOrder();
System.out.println("=====创建第二个对象=====");
InitOrder obj2 = new InitOrder();
}
}输出结果:
静态变量初始化
静态代码块执行
=====创建第一个对象=====
实例变量初始化
构造代码块执行
构造方法执行
=====创建第二个对象=====
实例变量初始化
构造代码块执行
构造方法执行练习
给Person类添加两个构造器:
- 无参构造器:设置
age初始值为 18。 - 带
pName和pAge参数的构造器:初始化name和age。
public class ConstructorExercise {
public static void main(String[] args) {
Person p1 = new Person(); // 调用无参构造器
System.out.println("p1 的信息name=" + p1.name + " age=" + p1.age); // name=null,age=18
Person p2 = new Person("scott", 50); // 调用带参构造器
System.out.println("p2 的信息name=" + p2.name + " age=" + p2.age); // name=scott,age=50
}
}
class Person {
String name; // 默认值null
int age; // 默认值0
// 无参构造器:age初始值18
public Person() {
age = 18;
}
// 带参构造器:初始化name和age
public Person(String pName, int pAge) {
name = pName;
age = pAge;
}
}对象创建内存流程
对象创建内存流程:
- 加载 Person 类信息(Person.class),只会加载一次
- 在堆中分配空间(地址)
- 完成对象初始化
- 3.1 默认初始化:age=0,name=null
- 3.2 显式初始化:age=90,name=null
- 3.3 构造器的初始化:age=20,name=小倩
- 将对象在堆中的地址,返回给 p(p 是对象名,也可以理解成是对象的引用)
.jpg)
this
概述
this:是 Java 保留关键字,有两种核心语义:
- 实例上下文:在实例方法 / 构造方法中,
this指代调用当前方法的对象实例(或正在初始化的对象实例),可通过this访问对象的成员变量 / 方法; - 构造调用:在构造方法中,
this(参数)用于调用本类的其他构造方法,实现构造逻辑复用。
本质:实例方法的隐式参数
Java 中所有实例方法(非 static 方法)都会隐式接收一个 this 参数,该参数由 JVM 在调用方法时自动传递,指向调用此方法的对象实例。
示例:字节码编译验证
从字节码层面看,实例方法的第一个参数永远是 this(类型为当前类),比如:
// 源码
public class Person {
private String name;
public void setName(String name) {
this.name = name;
}
}
// 编译后的字节码(简化):setName方法的第一个参数是this
// void setName(LPerson; Ljava/lang/String;)VJVM 调用 person.setName("张三") 时,会将 person 对象的引用作为 this 参数传递给 setName 方法,因此方法内可通过 this 访问该对象的 name 成员。

使用场景
this 的核心使用场景:
场景 1:区分同名的局部变量(形参)与成员变量(属性)
这是
this最常用的场景:当方法的局部变量与类的成员变量同名时,this.成员变量明确指向成员变量,避免 “变量遮蔽”。javapublic class Person { private String name; private int age; // 形参name/age与成员变量同名,用this区分 public Person(String name, int age) { // this.name:成员变量;name:形参 this.name = name; this.age = age; } }场景 2:调用本类的实例方法
this.方法名()可显式调用本类的其他实例方法,虽然省略this也能调用,但显式使用可增强代码可读性(尤其是区分继承的方法时)。javapublic class Calculator { private int result; public void add(int num) { this.result += num; } public void multiply(int num) { // 显式调用本类的add方法(省略this也可,但this增强语义) this.add(num); this.result *= num; } }场景 3:调用本类的其他构造方法
构造方法中通过
this(参数)调用本类的重载构造方法,实现构造逻辑复用,核心规则:this()必须是构造方法的第一条语句;this()的参数列表需匹配本类的某个构造方法;- 不能递归调用(如构造 A 调
this(),而this()又调回构造 A); - 不能与
super()同时使用(二者都需是第一条语句)。 this()语法只能在构造器中使用,不能在其他成员方法中使用。
javapublic class User { private String username; private String password; private String email; // 两参构造:调用三参构造,email传默认值 public User(String username, String password) { this(username, password, "default@xxx.com"); // 必须在第一行 System.out.println("两参构造执行"); } // 核心三参构造:初始化所有成员 public User(String username, String password, String email) { this.username = username; this.password = password; this.email = email; System.out.println("三参构造执行"); } public static void main(String[] args) { User u2 = new User("李四", "654321"); // 输出顺序: // 三参构造执行 → 两参构造执行 } }场景 4:返回当前对象(链式调用)
在实例方法中返回
this(当前对象引用),可实现 “链式调用”(如 Builder 模式、流式编程),简化代码调用。javapublic class StringBuilderDemo { private StringBuilder sb = new StringBuilder(); // 返回this,支持链式调用 public StringBuilderDemo append(String str) { sb.append(str); return this; // 返回当前对象 } public StringBuilderDemo append(int num) { sb.append(num); return this; } public String getResult() { return sb.toString(); } public static void main(String[] args) { // 链式调用:连续调用append方法 String result = new StringBuilderDemo() .append("Hello") .append("Java") .append(2025) .getResult(); System.out.println(result); // HelloJava2025 } }场景 5:将当前对象作为参数传递
this可作为实参传递给其他方法,将当前对象引用传递出去,适用于 “对象协作” 场景(如回调、依赖注入)。java// 工具类:接收Person对象并打印信息 class PersonUtil { public static void printPerson(Person person) { System.out.println("姓名:" + person.getName() + ",年龄:" + person.getAge()); } } public class Person { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } public void showInfo() { // 将当前对象(this)传递给PersonUtil的printPerson方法 PersonUtil.printPerson(this); } // getter public String getName() { return this.name; } public int getAge() { return this.age; } public static void main(String[] args) { Person p = new Person("王五", 25); p.showInfo(); // 输出:姓名:王五,年龄:25 } }场景 6:内部类中访问外部类的 this
非静态内部类(成员内部类 / 局部内部类)有自己的
this(指向内部类实例),若需访问外部类的this,需通过外部类名.this显式指定。javapublic class Outer { private String outerVar = "外部类变量"; // 成员内部类 class Inner { private String innerVar = "内部类变量"; private String outerVar = "内部类覆盖的outerVar"; public void show() { // 1. this:指向内部类实例 System.out.println(this.innerVar); // 内部类变量 System.out.println(this.outerVar); // 内部类覆盖的outerVar // 2. Outer.this:指向外部类实例 System.out.println(Outer.this.outerVar); // 外部类变量 } } public static void main(String[] args) { Outer outer = new Outer(); Outer.Inner inner = outer.new Inner(); inner.show(); } }
核心规则
this 的核心规则(必守):
规则 1:不能在静态上下文使用 this
静态上下文(静态方法、静态代码块、静态内部类)属于类级别,无对象实例,因此
this无指向,编译直接报错。javapublic class ThisStaticError { private static String staticVar = "静态变量"; private String instanceVar = "实例变量"; // 1. 静态方法:不能使用this public static void staticMethod() { // System.out.println(this.instanceVar); // 错误:Cannot use this in a static context // System.out.println(this); // 错误:同上 } // 2. 静态代码块:不能使用this static { // System.out.println(this.staticVar); // 错误:同上 } // 3. 静态内部类:不能访问外部类的this static class StaticInner { public void show() { // System.out.println(ThisStaticError.this.instanceVar); // 错误:静态内部类无外部this } } }规则 2:this () 调用构造方法的限制
this()必须是构造方法的第一条语句,否则编译报错;this()不能递归调用(如public Person() { this(); }会无限递归,编译报错);this()的参数列表必须匹配本类的某个构造方法(参数个数 / 类型 / 顺序一致);- 不能同时使用
this()和super()(二者都需是第一条语句,冲突)。
javapublic class ThisConstructorRule { public ThisConstructorRule() { // System.out.println("非第一条语句"); // 错误:this()必须是第一条 this(10); } public ThisConstructorRule(int num) { // this(); // 错误:递归调用构造方法 } // public ThisConstructorRule(String str) { // super(); // 错误:不能同时用super()和this() // this(10); // } }规则 3:this 指向调用方法的对象实例
同一个类的不同对象调用同一实例方法,
this指向不同的实例,保证每个对象的成员独立。可以通过.hashCode()判断 this 指向不同的内存地址。javapublic class ThisPointDemo { private int num; public void showThis() { System.out.println("this的引用地址:" + this.hashCode()); } public static void main(String[] args) { ThisPointDemo obj1 = new ThisPointDemo(); ThisPointDemo obj2 = new ThisPointDemo(); obj1.showThis(); // this指向obj1(地址不同) obj2.showThis(); // this指向obj2(地址不同) } }规则 4:this 不能被赋值
this是关键字,不是可赋值的变量,试图给this赋值会编译报错:javapublic class ThisAssignError { public void assignThis() { // 错误:The left-hand side of an assignment must be a variable // this = new ThisAssignError(); } }规则 5:this 永远不为 null(合法调用下)
实例方法必须通过对象调用(
obj.method()),JVM 会将obj作为this传递,因此合法调用下this不可能为null。例外:通过反射非法调用实例方法时,可传递
null作为this,此时方法内使用this会抛出NullPointerException:javaimport java.lang.reflect.Method; public class ThisNullReflect { public void show() { System.out.println(this); // 反射传null时,this为null → NPE } public static void main(String[] args) throws Exception { Method method = ThisNullReflect.class.getMethod("show"); method.invoke(null); // 传递null作为this → 运行时NPE } }
练习
需求:定义 Person 类,提供compareTo()方法,判断两个 Person 对象的名字和年龄是否完全一致。
public class TestPerson {
public static void main(String[] args) {
Person p1 = new Person("mary", 20);
Person p2 = new Person("mary", 20);
System.out.println("p1 和 p2 比较的结果:" + p1.compareTo(p2)); // true
}
}
class Person {
String name;
int age;
// 构造器
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// 比较方法
public boolean compareTo(Person p) {
// this 代表当前对象(调用compareTo方法的对象)
return this.name.equals(p.name) && this.age == p.age;
}
}本章作业
- 编写类 A01,定义方法
max(),实现求某个 double 数组的最大值,并返回(Homework01.java) - 编写类 A02,定义方法
find(),实现查找某字符串是否在字符串数组中,并返回索引;找不到返回-1(Homework02.java) - 编写类 Book,定义方法
updatePrice(),更改书的价格:- 价格 >150 → 150
- 价格 >100 → 100
- 否则不变(Homework03.java)
- 编写类 A03,实现数组复制功能
copyArr(),输入旧数组,返回新数组(元素和旧数组一致)(Homework04.java) - 定义圆类 Circle,属性:半径;提供方法:显示圆周长、显示圆面积(Homework05.java)
- 创建 Calc 计算类,定义两个操作数,提供加减乘除方法(除数为 0 时提示),创建对象测试(Homework06.java)
- 设计 Dog 类,属性:名字、颜色、年龄;定义
show()方法显示信息(使用 this.属性)(Homework07.java) - 以下代码编译运行后,输出结果是(
10,9,10)javapublic class Test { int count = 9; public void count1() { count = 10; System.out.println("count1=" + count); } public void count2() { System.out.println("count1=" + count++); } public static void main(String args[]) { new Test().count1(); // count1=10 Test t1 = new Test(); t1.count2(); // count1=9(后置++,先输出后自增) t1.count2(); // count1=10 } } - 定义 Music 类,属性:音乐名 name、时长 times;方法:播放 play()、返回属性信息 getInfo()(Homework09.java)
- 以下代码运行结果是(
101,100,101,101)javaclass Demo { int i = 100; public void m() { int j = i++; // j=100,i=101 System.out.println("i=" + i); // 101 System.out.println("j=" + j); // 100 } } class Test { public static void main(String[] args) { Demo d1 = new Demo(); Demo d2 = d1; // 引用传递,指向同一个对象 d2.m(); System.out.println(d1.i); // 101 System.out.println(d2.i); // 101 } } - 调用语句
System.out.println(method(method(10.0,20.0),100));编译正确,method方法的定义形式为:javapublic double method(double d1, double d2) { ... } - 创建 Employee 类,属性:名字、性别、年龄、职位、薪水;提供 3 个构造器(复用构造器):
- 构造器 1:初始化所有属性
- 构造器 2:初始化名字、性别、年龄
- 构造器 3:初始化职位、薪水(Homework12.java)
- 将对象作为参数传递给方法(Homework13.java)
- 定义 Circle 类:属性 radius,方法
findArea()返回面积 - 定义 PassObject 类:方法
printAreas(Circle c, int times),打印 1~times 的半径及对应面积 - 测试类中调用
printAreas(),输出结果如下:Radius 1.0 → Area 3.141592653589793 Radius 2.0 → Area 12.566370614359172 Radius 3.0 → Area 28.274333882308138 Radius 4.0 → Area 50.26548245743669 Radius 5.0 → Area 78.53981633974483
- 定义 Circle 类:属性 radius,方法
- 扩展题:设计 Tom 类,实现与电脑猜拳功能(石头 0、剪刀 1、布 2),记录输赢次数(Homework14.java)